<template>
  <div>
    <input type="file" @change="handleFileChange" />
    <el-button @click="handleUpload"> 上传文件 </el-button>
    <el-button @click="handleDelete"> 删除文件 </el-button>
  </div>
  <div>
    <div>
      <div>切片hash计算进度</div>
      <el-progress :percentage="hashPercentage"></el-progress>
    </div>
    <div>
      <div>文件上传进度</div>
      <el-progress :percentage="uploadPercentage"></el-progress>
    </div>
  </div>
  <el-table :data="data">
    <el-table-column
      prop="hash"
      label="切片名 hash"
      align="center"
    ></el-table-column>
    <el-table-column label="大小(KB)" align="center" width="120">
      <template v-slot="{ row }">
        {{ row.size | transformByte }}
      </template>
    </el-table-column>
    <el-table-column label="进度" align="center">
      <template v-slot="{ row }">
        <el-progress
          :percentage="row.percentage"
          color="#909399"
        ></el-progress>
      </template>
    </el-table-column>
  </el-table>
</template>

<script>
import { ElButton, ElMessage } from 'element-plus';
import 'element-plus/es/components/message/style/css'

// 切片大小 10M
const SIZE = 10 * 1024 * 1024

export default {
  components: { ElButton },
  data: () => ({
    container: {
      file: null,
    },
    data: []
  }),
  computed: {
    // 计算总进度 针对每一个 chunk的进度 计算出总的上传进度
    uploadPercentage() {
      console.log('计算属性发生变化')
      if (!this.container.file || !this.data.length) return 0;
      this.data.map((item) => {
        console.log(item)
      })
      const loaded = this.data
        .map(item => item.size * item.percentage)
        .reduce((acc, cur) => acc + cur);
      return parseInt((loaded / this.container.file.size).toFixed(2));
    }
  },
  methods: {
    // -------------------选择文件触发
    handleFileChange(e) {
      const [file] = e.target.files;
      console.log(e.target.files, file);
      if (!file) return;
      // this.$data是当前组件的响应式值，this.$options.data()是组件原本的值
      // Object.assign() 可以实现两个对象的合并，也就是将data初始化成原先的值
      Object.assign(this.$data, this.$options.data());
      this.container.file = file;
    },
    // 文件删除
    async handleDelete() {
      let { data } = await this.request({
        url: "http://localhost:3000/delete"
      });
      data = JSON.parse(data)
      // console.log(data)
      if (data.code === 0) {
        ElMessage.success(data.message)
      }
    },
    // --------------------发送请求
    // XMLHttpRequest 原生支持上传进度的监听，只需要监听 upload.onprogress 即可
    request({ url, method = 'post', data, headers = {}, onProgress = e => e, requestList }) {
      return new Promise((resolve) => {
        const xhr = new XMLHttpRequest();
        xhr.upload.onprogress = onProgress;
        xhr.open(method, url);
        Object.keys(headers).forEach((key) =>
          xhr.setRequestHeader(key, headers[key])
        );
        xhr.send(data);
        xhr.onload = (e) => {
          resolve({
            data: e.target.response,
          });
        };
      });
    },
    // ---------------------文件上传按钮触发
    async handleUpload() {
      if (!this.container.file) return
      const fileChunkList = this.createFileChunk(this.container.file)
      this.data = fileChunkList.map(({ file }, index) => ({
        chunk: file,
        index,
        // 文件名 + 数组下标
        // 在生成文件切片时，需要给每个切片一个标识作为 hash，这里暂时使用文件名 + 下标，这样后端可以知道当前切片是第几个切片，用于之后的合并切片
        hash: this.container.file.name + '-' + index,
        size: file.size,
        percentage: 0
      }))
      console.log('this.data===>>>', this.data)
      // 切片完成后调用 uploadChunks 上传所有的文件切片
      await this.uploadChunks()
    },
    // 计算单个切片时间
    // 每个切片在上传时都会通过监听函数更新 data 数组对应元素的 percentage 属性
    createProgressHandler(item) {
      return e => {
        item.percentage = parseInt(String((e.loaded / e.total) * 100));
      };
    },
    // ---------------------生成文件切片 内使用 while 循环和 slice 方法将切片放入 fileChunkList 数组中返回
    createFileChunk(file, size = SIZE) {
      const fileChunkList = []
      let cur = 0
      console.log('file.size==>', file.size)
      while(cur < file.size) {
        fileChunkList.push({ file: file.slice(cur, cur + size) })
        cur += size
      }
      console.log('fileChunkList==>', fileChunkList)
      return fileChunkList
    },
    // ----------------------上传切片, 同时过滤掉已上传的切片
    // uploadChunks 上传所有的文件切片，将文件切片，切片 hash，以及文件名放入 formData 中，再调用上一步的 request 函数返回一个 proimise，最后调用 Promise.all 并发上传所有的切片
    async uploadChunks() {
      const requestList = this.data
      .map(({ chunk, hash, index }) => {
        const formData = new FormData()
        formData.append("chunk", chunk)
        formData.append("hash", hash)
        formData.append("filename", this.container.file.name)
        console.log('formData===>>>', formData)
        return { formData, index }
      })
      .map(({ formData, index }) => 
        this.request({
          url: "http://localhost:3000/upload",
          data: formData,
          onProgress: this.createProgressHandler(this.data[index]),
        })
      )
      // 并发请求上传所有的切片
      await Promise.all(requestList)
      // 合并切片请求
      await this.mergeRequest()
    },
    // ---------------------发送合并切片请求
    async mergeRequest() {
      console.log('合并切片请求');
      await this.request({
        url: 'http://localhost:3000/merge',
        headers: {
          "content-type": "application/json"
        },
        data: JSON.stringify({
          size: SIZE,
          filename: this.container.file.name
        })
      });
    }
  }
};
</script>
